import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 基于Redis的分布式锁
 *
 * @author zkl
 * @data 2018/09/11
 */
public final class RedisLockUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockUtil.class);
    /**
     * 默认加锁时长是3000毫秒
     */
    private static final int DEFAULT_EXPIRE = 3000;
    /**
     * 默认空循环时间100毫秒
     */
    private static final int DEFAULT_SLEEP = 100;
    /**
     * 默认空循环时间最大时间3000毫秒
     */
    private static final int DEFAULT_LIMIT = 30000;
    /**
     * 加锁键前缀
     */
    private static final String LOCK_KEY = "LOCK:KEY:";
    /**
     * redis客户端
     */
    private static RedisCache redisCache;

    private RedisLockUtil() {

    }

    public static void setRedisCache(RedisCache redisCache){
        RedisLockUtil.redisCache = redisCache;
    }

    static {
        RedisLockInit.init();
    }
    /**
     * 加锁
     *
     * @param key    redis key
     * @param expire 过期时间，单位秒
     * @return true:加锁成功，false，加锁失败
     */
    public static boolean lock(String key, int expire, long sleep) {
        long value = System.currentTimeMillis() + expire;
        long status = redisCache.setnx(key, String.valueOf(value));

        if (status == 1) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("key【{}】，当前无锁直接获取。", key);
            }
            return true;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("key【{}】，当前有锁尝试循环获取。", key);
        }
        long limit = 0;
        int i = 0;
        for (; ; ) {
            i++;
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("key【{}】，第{}次尝试获取锁。", key, i);
            }
            boolean isGet = getSet(key, expire);
            if (isGet) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("key【{}】，第{}次尝试获取锁成功。", key, i);
                }
                return true;
            }
            if (sleep <= 0) {
                sleep = DEFAULT_SLEEP;
            }
            if (limit >= DEFAULT_LIMIT) {
                LOGGER.warn("key【{}】，第{}次获取锁失败，超过界定时间{}毫秒", new Object[]{key, i, DEFAULT_LIMIT});
                return false;
            }
            limit += sleep;
            try {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("key【{}】，第{}次获取锁失败，线程进入循环等待，休眠{}毫秒", new Object[]{key, i, sleep});
                }
                Thread.sleep(sleep);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 设置锁信息
     *
     * @param key
     * @param expire
     * @return
     */
    private static boolean getSet(String key, int expire) {
        String oldValue = redisCache.get(key);
        long oldExpireTime = oldValue != null ? Long.parseLong(oldValue) : 0L;
        if (oldExpireTime <= System.currentTimeMillis()) {
            //超时
            long newExpireTime = System.currentTimeMillis() + expire;
            long currentExpireTime = Long.parseLong(redisCache.getSet(key, String.valueOf(newExpireTime)));
            if (currentExpireTime == oldExpireTime) {
                return true;
            }
        }
        return false;
    }

    /**
     * 解锁
     *
     * @param key
     */
    public static void unLock(String key) {
        String value = redisCache.get(key);
        long oldExpireTime = value != null ? Long.parseLong(value) : 0L;
        long currTime = System.currentTimeMillis();
        if (oldExpireTime > currTime) {
            redisCache.del(key);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("key【{}】，解锁成功。", key);
            }
        } else {
            LOGGER.warn("key【{}】，锁自动过期，请检查过期时间！当前时间{}，过期时间{}", key, new Object[]{currTime, oldExpireTime});
        }
    }

    /**
     * 加锁
     * @param id
     * @return
     */
    public static boolean lock(String id) {
        // true表示获取锁，false标识未获取锁。如果锁未获取则不需要解锁
        boolean isLock = false;
        try {
            long end;
            long start = System.currentTimeMillis();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("业务ID：{}，准备获取锁", id);
            }
            // 加锁失败返回失败，防止堵塞
            if (lock(LOCK_KEY + id, DEFAULT_EXPIRE, DEFAULT_SLEEP)) {
                if (LOGGER.isDebugEnabled()) {
                    end = System.currentTimeMillis();
                    LOGGER.debug("企业ID：{}，获取锁成功，加锁执行时长{}毫秒", id, (end - start));
                }
                isLock = true;
            } else {
                end = System.currentTimeMillis();
                LOGGER.error("企业ID：{}，获取锁失败，加锁执行时长{}毫秒", id, (end - start));
            }
        } catch (Exception e) {
            LOGGER.error("企业ID：{}，获取锁异常", e);
        }
        return isLock;
    }

    /**
     * 解锁
     *
     * @param id
     * @param time
     */
    public static void unlock(String id, long time) {
        try {
            unLock(LOCK_KEY + id);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("业务ID：{}，解锁成功，业务执行时长{}毫秒", id, (System.currentTimeMillis() - time));
            }
        } catch (Exception e) {
            LOGGER.error("业务ID：{}，解锁异常", e);
        }
    }

    /**
     * 加锁执行
     *
     * @return
     */
    public static void userLock(String key, LockHandle handle) {
        long start = 0;
        // true表示获取锁，false标识未获取锁。如果锁未获取则不需要解锁
        boolean isLock = false;
        try {
            isLock = lock(key);
            if (!isLock) {
                return;
            }
            start = System.currentTimeMillis();

            //业务部分
            handle.handle();
        } finally {
            if (isLock) {
                unlock(key, start);
            }
        }
    }

    public interface LockHandle {
        void handle();
    }

    public interface RedisCache{
        long setnx(String k,String v);
        String getSet(String k,String v);
        String get(String k);
        long del(String k);
    }
}